#include <QFileInfo>
#include <QDateTime>
#include <QDebug>
#include <cinttypes>
#include <cwchar>
#include "msgpackrequest.h"
#include "tree.h"
#include <QElapsedTimer>

#if defined(Q_OS_WIN)
extern int mk_wcwidth(wchar_t ucs);
#endif
using std::string;
namespace NeovimQt {

Tree::ClipboardMode Tree::paste_mode;
QStringList Tree::clipboard;
NeovimApi6 *Tree::api;
const int kStop = 90;
Tree::Tree()
{
}

Tree::~Tree()
{
    erase_entrylist(0, m_fileitem.size());
}
Tree::Tree(int bufnr, int ns_id)
    : bufnr(bufnr), icon_ns_id(ns_id)
{

    api->nvim_buf_set_option(bufnr, "ft", "tree");
    api->nvim_buf_set_option(bufnr, "modifiable", false);

    api->nvim_command("lua tree = require('tree')");
    // b->nvim_buf_attach(bufnr, false, {});
    api->nvim_execute_lua("tree.buf_attach(...)", {bufnr});
}

int countgrid(const QString &s)
{
    int n = s.count();
    int ans = 0;
    for (int i = 0; i < n; i++) {
        const QChar c = s.at(i);
        ushort uNum = c.unicode();
        // Characters can be full-width or half-width (with invisible characters being 0-width).
        // Ref:
        //  https://bugreports.qt.io/browse/QTBUG-9956
        //  https://www.php.net/manual/en/function.mb-strwidth.php
        //  https://www.ibm.com/support/knowledgecenter/beta/fi/ssw_ibm_i_74/rtref/wcwidth.htm
        //  https://embedeo.org/ws/doc/man_windows/
        wchar_t wc = uNum;
        // qDebug() << QString(c) << wcwidth(wta);
#if defined(Q_OS_WIN)
        if (mk_wcwidth(wc)==2) {
            ans++;
        }
#else
        if (wcwidth(wc)==2) {
            ans++;
        }
#endif
    }
    return n + ans;  // screen cells
}
void truncate(const int l, Cell &cell)
{
    int old_bytesize = cell.text.size();
    int n = cell.col_end - cell.col_start - l;
    int total = 0; // total visual width
    int pos = 1;
    QString str(cell.text);
    while (total <= n) {
#if defined(Q_OS_WIN)
        total += mk_wcwidth(str.at(pos).unicode());
#else
        total += wcwidth(str.at(pos).unicode());
#endif
        str.remove(pos, 1);
    }
    str.insert(pos, "…");
    cell.text = str.toUtf8();
    cell.byte_end -= old_bytesize - cell.text.size();
    cell.col_end -= total - 1;
}

void Tree::makeline(const int pos, QByteArray &line)
{
    assert(0<=pos&&pos<col_map[FILENAME].size());
    int start = 0;
    foreach (const int col, cfg.columns) {
        const Cell & cell = col_map[col][pos];
        line.append(QString(cell.col_start-start, ' '));
        line.append(cell.text);
        int len = cell.byte_end - cell.byte_start - cell.text.size();
        // QString cell_str(cell.text);
        // if (col=="filename")
        //     len = cell.col_end-countgrid(cell_str)-cell.col_start;
        // else
        //     len = cell.col_end-cell_str.size()-cell.col_start;
        QString spc_after(len, ' ');
        line.append(std::move(spc_after));
        start = cell.col_end;
    }
    // qDebug()<<"mkline"<<pos<<":"<<line;
}
void Tree::changeRoot(QString path)
{
    // TODO: cursor history
    QDir dir(path);
    if (!dir.exists()) {
        return;
    }
    const QString & rootPath = dir.absolutePath();
    expandStore.insert(rootPath, true);

    // QDir::NoSymLinks

    // QFileInfo root_fi(path);
    FileItem::update_gmap(path);
    targets.clear();
    erase_entrylist(0, m_fileitem.size());

    FileItem *fileitem = new FileItem;
    fileitem->level = -1;
    fileitem->opened_tree = true;
    fileitem->fi = QFileInfo(dir.absolutePath());
    m_fileitem.append(fileitem);

    insert_rootcell(0);
    // FIXME: when icon not available
    // col_map["icon"][0].text = "";

    QList<QByteArray> ret;
    QByteArray line;
    makeline(0, line);
    ret.append(line);

    QList<FileItem*> child_fileitem;
    entryInfoListRecursively(*m_fileitem[0], child_fileitem);
    for (int i=0;i<child_fileitem.size();++i)
        m_fileitem.insert(1+i, child_fileitem[i]);

    insert_entrylist(child_fileitem, 1, ret);

    buf_set_lines(0, -1, true, std::move(ret));
    QString k = (*m_fileitem[0]).fi.absoluteFilePath();
    if (cursorHistory.contains(k)) {
        api->nvim_win_set_cursor(0, QPoint{0, cursorHistory[k]});
    }

    hline(0, m_fileitem.size());
}

/// Insert columns
void Tree::insert_item(const int pos)
{
    const FileItem &fileitem = *m_fileitem[pos];
    int start = 0;
    int byte_start = 0;
    foreach (const int col, cfg.columns) {
        Cell cell(cfg, fileitem, col);
        QString cell_str(cell.text);
        cell.byte_start = byte_start;
        cell.byte_end = byte_start+cell.text.size();
        cell.col_start = start;
        if (col==FILENAME) {
            cell.col_end = start + countgrid(cell_str);
        }
        else {
            cell.col_end = start + cell_str.size();
        }

        // NOTE: alignment
        if (col==FILENAME) {
            int tmp = kStop - cell.col_end;
            // TODO:此处都设置成统一列，在makeline时进行截断
            if (tmp > 0) {
                cell.col_end+=tmp;
                cell.byte_end+=tmp;
            }
            else if (kStop>cell.col_start+5) {
                truncate(kStop - cell.col_start, cell);
                int tmp = kStop - cell.col_end;
                if (tmp > 0) {
                    cell.col_end+=tmp;
                    cell.byte_end+=tmp;
                }
            }
        }

        int sep = (col==INDENT?0:1);
        start = cell.col_end + sep;
        byte_start = cell.byte_end + sep;
        if (!col_map.contains(col)) {
            col_map.insert(col, QList<Cell>());
        }
        // TODO: 考虑编译器优化
        col_map[col].insert(pos, std::move(cell));
    }
}

void Tree::insert_rootcell(const int pos)
{
    const FileItem &fileitem = *m_fileitem[pos];
    int start = 0;
    int byte_start = 0;
    foreach (const int col, cfg.columns) {
        Cell cell(cfg, fileitem, col);
        cell.col_start = start;
        cell.byte_start = byte_start;
        if (col==FILENAME) {
            QByteArray filename(fileitem.fi.absoluteFilePath().toUtf8());
            if (!fileitem.fi.isRoot() && fileitem.fi.isDir()) {
                filename.append("/");
            }
            filename.prepend(cfg.root_marker.c_str());
            cell.text = filename;
        }
        QString cell_str(cell.text);
        cell.byte_end = byte_start+cell.text.size();

        if (col==FILENAME) {
            cell.col_end = start + countgrid(cell_str);
        }
        else {
            cell.col_end = start + cell_str.size();
        }

        // NOTE: alignment
        if (col==FILENAME) {
            int tmp = kStop - cell.col_end;
            // TODO:此处都设置成统一列，在makeline时进行截断
            if (tmp >0) {
                cell.col_end+=tmp;
                cell.byte_end+=tmp;
            }
        }

        int sep = (col==INDENT?0:1);
        start = cell.col_end + sep;
        byte_start = cell.byte_end + sep;

        if (!col_map.contains(col)) {
            col_map.insert(col, QList<Cell>());
        }
        // TODO: 考虑编译器优化
        col_map[col].insert(pos, std::move(cell));
    }
}
/// pos is 0-based row number.
void Tree::insert_entrylist(const QList<FileItem*>& fil, const int pos, QList<QByteArray>& ret)
{
    int file_count = fil.count();
    for (int i = 0; i < file_count; ++i) {
        // QString absolute_file_path = file_info.absoluteFilePath();
        // QByteArray absolute_file_path(file_info.absoluteFilePath().toUtf8());

        insert_item(pos+i);

        QByteArray line;
        makeline(pos+i, line);
        ret.append(std::move(line));
        // if (QString::compare(suffix, QString("png"), Qt::CaseInsensitive) == 0) { }
    }
}
// TODO: 通过FileItem的parent查询
/// l is 0-based row number.
/// NOTE: root.level=-1
int Tree::find_parent(int l)
{
    int level = m_fileitem.at(l)->level;
    for (int i=l-1;i>=0;--i)
    {
        const FileItem &fn = *m_fileitem.at(i);
        if (fn.level == level-1)
            return i;
    }
    return -1;
}

/// [l, l] <=> no sub files; l is parent row number(0-based).
std::tuple<int, int> Tree::find_range(int l)
{
    int s=l, i;
    int level = m_fileitem.at(l)->level;
    for (i=l+1;i<m_fileitem.size();++i)
    {
        int l = m_fileitem.at(i)->level;
        if (level >= l)
            break;
    }
    return std::make_tuple(s, i-1);
}

/// 0-based [sl, el).
void Tree::hline(int sl, int el)
{
    int bufnr = this->bufnr;
    api->nvim_buf_clear_namespace(bufnr, icon_ns_id, sl, el);
    for (int i = sl;i<el;++i)
    {
        const FileItem &fileitem = *m_fileitem[i];
        char name[40];

        foreach (const int col, cfg.columns) {
            const Cell & cell = col_map[col][i];

            if(col==FILENAME) {
                sprintf(name, "tree_%d_%d", col, fileitem.fi.isDir());
                api->nvim_buf_add_highlight(bufnr, icon_ns_id, name, i, cell.byte_start, cell.byte_start+cell.text.size());
            } else if(col==ICON || col==GIT || col==MARK) {
                // :hi tree_<tab>
                sprintf(name, "tree_%d_%d", col, cell.color);
                // sprintf(name, "tree_%s", cell.text.data());
                // qDebug() << icon.col_start<< "->"<<icon.col_end;
                // qDebug() << icon.text;
                // auto req_hl = api->nvim_buf_add_highlight(bufnr, 0, "String", 0, 0, 3);
                // call nvim_buf_add_highlight(0, -1, "Identifier", 0, 5, -1)
                api->nvim_buf_add_highlight(bufnr, icon_ns_id, name, i, cell.byte_start, cell.byte_start+cell.text.size());
            } else if (col==SIZE || col==TIME ){//|| col==INDENT
                sprintf(name, "tree_%d", col);
                api->nvim_buf_add_highlight(bufnr, icon_ns_id, name, i, cell.byte_start, cell.byte_start+cell.text.size());
            }
        }
    }
}

/// 0-based [sl, el).
void Tree::redraw_line(int sl, int el)
{
    char format[] = "%s (1-based): [%d, %d]";
    qDebug(format, __PRETTY_FUNCTION__, sl+1, el);

    QList<QByteArray> ret;
    for (int i = sl; i < el; ++i) {
        FileItem & fileitem = *m_fileitem[i];

        int start = 0;
        int byte_start = 0;
        foreach (const int col, cfg.columns) {
            Cell& cell = col_map[col][i];
            if (col==MARK){
            }else if(col==INDENT){
            }else if(col==GIT){
                // TODO: Git::update_gmap(fn);
                cell.update_git(fileitem.fi);
            }else if(col==ICON){
                cell.update_icon(fileitem);
            } else if(col==FILENAME){
            }else if(col==SIZE){
                cell.update_size(fileitem.fi);
            }
            QString cell_str(cell.text);
            cell.col_start =start;
            cell.col_end = start + countgrid(cell_str);
            cell.byte_start = byte_start;
            cell.byte_end = byte_start+cell.text.size();

            // qDebug() << col;

            if (col==FILENAME)
            {
                int tmp = kStop - cell.col_end;
                if (tmp >0)
                {
                    cell.col_end+=tmp;
                    cell.byte_end+=tmp;
                }
            }
            int sep = (col==INDENT?0:1);
            start = cell.col_end + sep;
            byte_start = cell.byte_end + sep;
        }

        QByteArray line;
        makeline(i, line);
        ret.append(std::move(line));
    }
    buf_set_lines(sl, el, true, std::move(ret));
    hline(sl, el);
}

/// redraw parent and children
/// assert l is 0-based parent index
void Tree::redraw_recursively(int l)
{
    assert(0 <= l && l < m_fileitem.size());
    FileItem &cur = *m_fileitem[l];

    std::tuple<int, int> se = find_range(l);
    int s = std::get<0>(se) + 1;
    int e = std::get<1>(se) + 1;
    qDebug("%s redraw range(1-based): [%d, %d]", __PRETTY_FUNCTION__, s+1, e);

    erase_entrylist(s, e);

    QList<FileItem*> child_fileitem;
    // const QString &p = cur.fi.absoluteFilePath();
    entryInfoListRecursively(cur, child_fileitem);
    int file_count = child_fileitem.size();
    for (int i = 0; i < file_count; ++i)
        m_fileitem.insert(l + 1 + i, child_fileitem[i]);

    if (file_count <= 0) {
        return;
    }

    QList<QByteArray> ret;
    insert_entrylist(child_fileitem, l + 1, ret);
    buf_set_lines(s, e, true, ret);
    hline(l + 1, l + 1 + ret.size());

    return;
}
/// erase [s, e)
void Tree::erase_entrylist(const int s, const int e)
{
    foreach (const int col, cfg.columns) {
        QList<Cell> & cell = col_map[col];
        cell.erase(cell.begin()+s, cell.begin()+e);
    }
    for (int i = s; i < e; ++i) {
        delete m_fileitem.at(i);
    }
    m_fileitem.erase(m_fileitem.begin()+s, m_fileitem.begin()+e);
}

// get entryInfoList recursively
void Tree::entryInfoListRecursively(const FileItem& item,
                                   QList<FileItem*> &fileitem_lst)
{
    QDir dir(item.fi.absoluteFilePath());
    set_dir(dir);
    const int level = item.level+1;
    QFileInfoList child = dir.entryInfoList();
    int file_count = child.count();
    if (file_count <= 0) {
        return;
    }
    for (int i = 0; i < file_count; ++i) {
        QFileInfo fi = child.at(i);

        FileItem *fileitem = new FileItem;
        fileitem->fi = fi;
        fileitem->level = level;
        fileitem->parent = &item;
        if (i==file_count-1)
            fileitem->last=true;
        QString p = fi.absoluteFilePath();

        if (expandStore.contains(p) && expandStore[p]) {
            fileitem->opened_tree = true;
            fileitem_lst.append(fileitem);
            entryInfoListRecursively(*fileitem, fileitem_lst);
        }
        else {
            fileitem_lst.append(fileitem);
        }
    }
}


// shrink recursively
void Tree::shrinkRecursively(const QString &path)
{
    QDir dir(path);
    set_dir(dir);
    QFileInfoList child = dir.entryInfoList();
    int file_count = child.count();
    if (file_count <= 0) {
        return;
    }
    for (int i = 0; i < file_count; ++i) {
        QFileInfo file_info = child.at(i);
        QString p = file_info.absoluteFilePath();

        if (expandStore.contains(p) && expandStore[p]) {
            expandStore[p] = false;
            shrinkRecursively(p);
        }
    }
    return;
}

// shrink recursively
void Tree::expandRecursively(const FileItem &item,
                            QList<FileItem*> &fileitem_lst)
{
    QDir dir(item.fi.absoluteFilePath());
    set_dir(dir);
    const int level = item.level+1;
    QFileInfoList child = dir.entryInfoList();
    int file_count = child.count();
    if (file_count <= 0) {
        return;
    }
    for (int i = 0; i < file_count; ++i) {
        QFileInfo file_info = child.at(i);

        FileItem *fileitem = new FileItem;
        fileitem->fi = file_info;
        fileitem->level = level;
        fileitem->parent = &item;
        if (i==file_count-1)
            fileitem->last=true;

        if (file_info.isDir()) {
            QString p = file_info.absoluteFilePath();
            expandStore.insert(p, true);
            fileitem->opened_tree = true;
            fileitem_lst.append(fileitem);
            expandRecursively(*fileitem, fileitem_lst);
        }
        else {
            fileitem_lst.append(fileitem);
        }
    }
    return;
}

void Tree::handleRename(const QVariant& val)
{
    disconnect(api, &NeovimApi6::on_nvim_call_function, this, &Tree::handleRename);
    qDebug() << __PRETTY_FUNCTION__;
    input = val.toString();

    Cell & cur = col_map[FILENAME][ctx.cursor-1];
    FileItem & item = *m_fileitem[ctx.cursor-1];
    if (item.fi.isDir() && input.endsWith('/'))
        input.remove(input.size()-1, 1);
    QString fn = item.fi.absoluteFilePath();
    QFile qf(fn);
    if (qf.rename(input)) {
        api->nvim_execute_lua("tree.print_message(...)", {"Rename Success"});
        item.fi = QFileInfo(qf);
        QByteArray text(item.fi.fileName().toUtf8());
        if (item.fi.isDir())
            text.append("/");
        cur.text = text;

        // NOTE: gmap may update
        FileItem::update_gmap(item.fi.absolutePath());
        // redraw_line(ctx.cursor-1, ctx.cursor);
        // TODO: 细粒度redraw
        FileItem &root = *m_fileitem[0];
        changeRoot(root.fi.absoluteFilePath());
    } else{
        api->nvim_execute_lua("tree.print_message(...)", {"Rename failed"});
    }
}

void Tree::handleNewFile(const QVariant &val)
{
    disconnect(api, &NeovimApi6::on_nvim_call_function, this, &Tree::handleNewFile);

    input = val.toString();
    if (input=="") {
        api->nvim_execute_lua("tree.print_message(...)", {"Canceled"});
        return;
    }
    qDebug() << __PRETTY_FUNCTION__ << input;

    // Cell & cur = col_map["filename"][ctx.cursor-1];
    FileItem & item = *m_fileitem[ctx.cursor-1];

    // fileitem.fi.absolutePath() -> QString
    QDir dest = item.opened_tree?item.fi.absoluteFilePath():item.fi.absoluteDir();

    QFileInfo fi(dest.filePath(input));
    // NOTE: failed when same name file exists
    if (fi.exists()) {
        api->nvim_execute_lua("tree.print_message(...)", {"File already exists!"});
        return;
    }
    else if(input.endsWith('/')){
        if (!dest.mkpath(input))
            api->nvim_execute_lua("tree.print_message(...)", {"Failed to create dir!"});
    } else {
        QFile file(dest.filePath(input));
        file.open(QIODevice::WriteOnly);
        file.close();
    }

    if (item.opened_tree) {
        redraw_recursively(ctx.cursor-1);
    } else{
        // TODO: location: find_child
        int pidx = find_parent(ctx.cursor-1);
        // NOTE: root.level=-1
        // TODO: consider named redraw_family
        redraw_recursively(pidx);
    }
}

// XXX: It override the builtin 'input()' function.
void Tree::vim_input(string prompt="", string text="", string completion="", string handle="")
{
    qDebug() << __PRETTY_FUNCTION__;
    MsgpackRequest* req;
    if (handle=="rename")
        connect(api, &NeovimQt::NeovimApi6::on_nvim_call_function, this, &Tree::handleRename);
    else if(handle=="new_file")
        connect(api, &NeovimQt::NeovimApi6::on_nvim_call_function, this, &Tree::handleNewFile);
    QVariantList args = {prompt.c_str(), text.c_str(), completion.c_str()};
    // qDebug() << args;
    req = api->nvim_call_function("input", args);
    // NOTE: MsgpackRequest::finished different with on_nvim_call_function

    // if(completion!="")
    //     req = api->nvim_call_function("input", {prompt.c_str(), text.c_str(), completion.c_str()});
    // else
    //     req = api->nvim_call_function("input", {prompt.c_str(), text.c_str()});
}

/// 收集无序targets
/// 视图变化之后 targets 要更新
void Tree::collect_targets()
{
    targets.clear();
    for (int i = 0; i < m_fileitem.size(); ++i) {
        const FileItem &item = *m_fileitem[i];
        if (item.selected) {
            targets.append(i);
        }
    }
}

void Tree::save_cursor()
{
    FileItem &item = *m_fileitem[0];
    cursorHistory.insert(item.fi.absoluteFilePath(), ctx.cursor);
    qDebug() << "xxxx" << item.fi.absoluteFilePath() << ":"<< ctx.cursor;
}

void Tree::paste(const int ln, const QString &src, const QString &dest)
{
    QFileInfo fi(src);
    if (fi.isDir()) {
        if (paste_mode == COPY) {
            if (QFile::copy(src, dest)) {
                api->nvim_execute_lua("tree.print_message(...)", {"Copyed"});
            }
            qDebug()<<"Copy Paste dir";
            int pidx = find_parent(ln);
            redraw_recursively(pidx);
        }
        else if (paste_mode == MOVE){
            QDir dir;
            dir.rename(src, dest);
            qDebug()<<"Move Paste dir";
            FileItem &root = *m_fileitem[0];
            changeRoot(root.fi.absoluteFilePath());
        }
    }
    else if (fi.isFile()) {
        if (paste_mode == COPY) {
            if (QFile::copy(src, dest)) {
                api->nvim_execute_lua("tree.print_message(...)", {"Copyed"});
            }
            qDebug()<<"Copy Paste";
            int pidx = find_parent(ln);
            redraw_recursively(pidx);
        }
        else if (paste_mode == MOVE){
            QFile::rename(src, dest);
            qDebug()<<"Move Paste";
            FileItem &root = *m_fileitem[0];
            changeRoot(root.fi.absoluteFilePath());
        }
    }
    return;

    // QDir dir(src);
    // dir.removeRecursively();
    // if (!QFile::remove(src)) {
    //     qDebug()<<"Remove failed:"<<src;
    // }
}
typedef void (Tree::*Action)(const QList<QVariant>& args);
QHash<QString, Action> action_map {
    {"cd"                   , &Tree::cd},
    {"goto"                 , &Tree::goto_},
    {"open_or_close_tree"   , &Tree::open_tree},
    {"open"                 , &Tree::open},
    {"copy"                 , &Tree::copy},
    {"move"                 , &Tree::move},
    {"paste"                , &Tree::pre_paste},
    {"remove"               , &Tree::pre_remove},
    {"yank_path"            , &Tree::yank_path},
    {"toggle_select"        , &Tree::toggle_select},
    {"toggle_select_all"    , &Tree::toggle_select_all},
    {"print"                , &Tree::print},
    {"debug"                , &Tree::debug},
    {"toggle_ignored_files" , &Tree::toggle_ignored_files},
    {"redraw"               , &Tree::redraw},
    {"new_file"             , &Tree::new_file},
    {"execute_system"       , &Tree::execute_system},
    {"rename"               , &Tree::rename},
    {"drop"                 , &Tree::drop},
    {"call"                 , &Tree::call},
    {"open_tree_recursive"  , &Tree::open_or_close_tree_recursively},
};


void Tree::action(const QString &action, const QList<QVariant> &args,
                  const QMap<QString, QVariant> context)
{
    qDebug() << __PRETTY_FUNCTION__ << action;

    // qDebug() << "context:" << context;
    this->ctx = context;
    qDebug() << "cursor position(1-based): " << ctx.cursor;

    if (action_map.contains(action)) {
        (this->*action_map[action])(args);
    }
    else {
        api->nvim_execute_lua("tree.print_message(...)", {QString("Unknown Action: %1").arg(action)});
    }
}

void Tree::cd(const QList<QVariant>& args)
{
    save_cursor();
    if (args.size()>0) {
        QString dir = args.at(0).toString();

        if (dir=="..") {
            QDir curdir(m_fileitem.at(0)->fi.absoluteFilePath());
            if (curdir.cd(".."))
            {
                qDebug()<< __PRETTY_FUNCTION__ << curdir;
                changeRoot(curdir.absolutePath());
            }
        }
        else if (dir == ".") {
            FileItem &cur = *m_fileitem[ctx.cursor - 1];
            QString dir(cur.fi.absolutePath());
            if (cur.opened_tree)
                dir = cur.fi.absoluteFilePath();
            QString cmd = QString("cd %1").arg(dir);
            api->nvim_execute_lua("tree.print_message(...)", {cmd});
            api->nvim_command(cmd.toUtf8());
        }
        else {
            changeRoot(dir);
        }
    }
    else {
        changeRoot(QDir::home().absolutePath());
    }
}
void Tree::_toggle_select(const int pos)
{
    // TODO: mark may not available
    Cell &cur = col_map[MARK][pos];
    FileItem& item = *m_fileitem[pos];

    item.selected = !item.selected;
    if (item.selected) {
        cur.text = mark_indicators["selected_icon"];
        cur.color = BLUE;
        targets.append(pos);
    }
    else {
        cur.text=" ";
        cur.color = WHITE;
        targets.removeOne(pos);
    }

    redraw_line(pos, pos+1);
}
void Tree::toggle_select(const QList<QVariant>& args)
{
    const int pos = ctx.cursor - 1;
    _toggle_select(pos);
}

void Tree::rename(const QList<QVariant>& args)
{
    // qDebug() << action << args;
    FileItem &cur = *m_fileitem[ctx.cursor - 1];
    string info = cur.fi.absoluteFilePath().toStdString();
    // NOTE: specify handle for vim_input
    vim_input("Rename: " + info + " -> ", info, "file", "rename");
}
void Tree::redraw(const QList<QVariant>& args)
{
    FileItem &root = *m_fileitem[0];
    changeRoot(root.fi.absoluteFilePath());
}
void Tree::new_file(const QList<QVariant>& args)
{
    FileItem &cur = *m_fileitem[ctx.cursor - 1];
    string info = cur.fi.absolutePath().toStdString();
    vim_input("New File: " + info + "/", "", "file", "new_file");
}
void Tree::execute_system(const QList<QVariant>& args)
{
    FileItem &cur = *m_fileitem[ctx.cursor - 1];
    QString info = cur.fi.absoluteFilePath();
    api->nvim_call_function("tree#util#open", {info});
}
void Tree::toggle_ignored_files(const QList<QVariant>& args)
{
    cfg.show_ignored_files=!cfg.show_ignored_files;
    FileItem &root = *m_fileitem[0];
    changeRoot(root.fi.absoluteFilePath());
}
void Tree::debug(const QList<QVariant>& args)
{
    qDebug() << cfg.columns;
}
void Tree::drop(const QList<QVariant>& args)
{
    FileItem &cur = *m_fileitem[ctx.cursor - 1];
    QString info = cur.fi.absoluteFilePath();
    qDebug()<<args;
    if (cur.fi.isDir())
        changeRoot(cur.fi.absoluteFilePath());
    else {
        api->nvim_execute_lua("tree.drop(...)", {args, info});
    }
}
void Tree::print(const QList<QVariant>& args)
{
    FileItem &cur = *m_fileitem[ctx.cursor - 1];
    QString msg =cur.fi.absoluteFilePath();
    api->nvim_execute_lua("tree.print_message(...)", {msg});
}
void Tree::pre_paste(const QList<QVariant> &args)
{
    // TODO: 批量paste
    if (clipboard.size() <= 0) {
        api->nvim_execute_lua("tree.print_message(...)", {"Nothing in clipboard"});
        return;
    }
    foreach (const QString &f, clipboard) {
        FileItem &cur = *m_fileitem[ctx.cursor - 1];
        QString fname = QFileInfo(f).fileName();
        QDir curdir = cur.fi.absoluteDir();
        if (cur.opened_tree) curdir = QDir(cur.fi.absoluteFilePath());
        QFileInfo destfile(curdir.absoluteFilePath(fname));
        if (destfile.exists()) {
            // api->nvim_execute_lua("tree.print_message(...)", {"Destination file exists"});

            QVariantMap dest;
            dest["mtime"] = destfile.lastModified().toTime_t();
            dest["path"] = destfile.absoluteFilePath();
            dest["size"] = destfile.size();
            QFileInfo srcfile(f);
            QVariantMap src;
            src["mtime"] = srcfile.lastModified().toTime_t();
            src["path"] = srcfile.absoluteFilePath();
            src["size"] = srcfile.size();
            api->nvim_execute_lua(
                "tree.pre_paste(...)",
                {QVariantList({bufnr, QVariant(ctx.cursor - 1)}), dest, src});
        }
        else {
            paste(ctx.cursor-1, f, destfile.absoluteFilePath());
        }
    }
}

void Tree::yank_path(const QList<QVariant>& args)
{
    QStringList yank;
    foreach (const int &pos, targets) {
        yank.append(m_fileitem[pos]->fi.absoluteFilePath());
    }
    if (yank.size()==0) {
        FileItem &cur = *m_fileitem[ctx.cursor - 1];
        yank << cur.fi.absoluteFilePath();
    }
    QString reg = yank.join('\n');
    api->nvim_call_function("setreg", {"\"", reg});

    yank.insert(0, "yank_path");
    QString msg = yank.join('\n');
    api->nvim_execute_lua("tree.print_message(...)", {msg});
}

void Tree::pre_remove(const QList<QVariant> &args)
{
    QVariantMap farg;
    int cnt = targets.size();
    farg["cnt"] = cnt==0 ? 1:cnt;
    api->nvim_execute_lua("tree.pre_remove(...)", {bufnr, farg});
}
void Tree::remove()
{
    // TODO: remove 之后光标位置
    QStringList rmfiles;
    foreach (const int &pos, targets) {
        rmfiles.append(m_fileitem[pos]->fi.absoluteFilePath());
    }
    if (rmfiles.size()==0) {
        FileItem &cur = *m_fileitem[ctx.cursor - 1];
        rmfiles << cur.fi.absoluteFilePath();
    }
    foreach (const QString &f, rmfiles) {
        QFileInfo fi(f);
        if (fi.isDir()) {
            QDir dir(f);
            dir.removeRecursively();
        }
        else if (fi.isFile()) {
            if (!QFile::remove(f)) {
                qDebug()<<"Remove failed:"<<f;
            }
        }
    }
    FileItem &root = *m_fileitem[0];
    changeRoot(root.fi.absoluteFilePath());
}
void Tree::call(const QList<QVariant>& args)
{
    QString func = args.at(0).toString();
    FileItem &cur = *m_fileitem[ctx.cursor - 1];
    QVariantMap ctx;
    ctx["targets"] = QVariantList{cur.fi.absoluteFilePath()};
    api->nvim_call_function(func.toUtf8(), {ctx});
}
void Tree::toggle_select_all(const QList<QVariant>& args)
{
    for (int i=1;i<m_fileitem.size();++i) {
        _toggle_select(i);
    }
}
void Tree::copy(const QList<QVariant>& args)
{
    paste_mode = COPY;
    _copy_or_move(args);
    api->nvim_execute_lua("tree.print_message(...)", {"Copy to clipboard"});
}
void Tree::move(const QList<QVariant>& args)
{
    paste_mode = MOVE;
    _copy_or_move(args);
    api->nvim_execute_lua("tree.print_message(...)", {"Move to clipboard"});
}
void Tree::_copy_or_move(const QList<QVariant>& args)
{
    Tree::clipboard.clear();

    foreach (const int &pos, targets) {
        const FileItem *p = m_fileitem[pos]->parent;
        // NOTE: root item or parent selected
        if (p==nullptr || !p->selected)
            Tree::clipboard << m_fileitem[pos]->fi.absoluteFilePath();
    }
    if (clipboard.size()==0) {
        FileItem &cur = *m_fileitem[ctx.cursor - 1];
        Tree::clipboard << cur.fi.absoluteFilePath();
    }
    qDebug() << clipboard;
}

QMap<QString, QVariant> Tree::get_candidate(const int pos)
{
//  'word': 'column.cpp',
    FileItem & item = *m_fileitem[pos];
    return {
        {"is_directory", item.fi.isDir()},
        {"action__path", item.fi.absoluteFilePath()},
        {"level", item.level},
        {"is_opened_tree", item.opened_tree},
        {"is_selected", item.selected}
    };
}
/// l is 0-based row number.
void Tree::open_tree(const QList<QVariant> &args)
{
    const int l = ctx.cursor - 1;
    qDebug() << __PRETTY_FUNCTION__;
    assert(0 <= l && l < m_fileitem.size());
    // if (l == 0) return;
    QList<QByteArray> ret;
    FileItem &cur = *m_fileitem[l];

    if (cur.fi.isDir() && !cur.opened_tree) {

        cur.opened_tree = true;
        const QString & rootPath = cur.fi.absoluteFilePath();
        expandStore.insert(rootPath, true);
        redraw_line(l, l + 1);
        QList<FileItem*> child_fileitem;
        entryInfoListRecursively(cur, child_fileitem);
        int file_count = child_fileitem.size();
        for (int i=0;i<file_count;++i)
            m_fileitem.insert(l+1+i, child_fileitem[i]);

        if (file_count <= 0) {
            return;
        }

        QElapsedTimer mstimer;
        mstimer.start();
        insert_entrylist(child_fileitem, l + 1, ret);

        buf_set_lines(l+1, l+1, true, ret);
        hline(l + 1, l + 1 + ret.size());
        float time = (double)mstimer.nsecsElapsed()/(double)1000000;
        qDebug() << time; // ms

    }
    else if (cur.opened_tree) {
        const QString & p = cur.fi.absoluteFilePath();
        if (expandStore.contains(p) && expandStore[p]) {
            expandStore[p] = false;
        }
        std::tuple<int, int> se = find_range(l);
        int s = std::get<0>(se) + 1;
        int e = std::get<1>(se) + 1;
        qDebug("\tclose range(1-based): [%d, %d]", s+1, e);
        buf_set_lines(s, e, true, {});

        erase_entrylist(s, e);
        cur.opened_tree = false;
        redraw_line(l, l + 1);
    }
    else if (find_parent(l) >= 0) {
        int parent = find_parent(l);
        std::tuple<int, int> se = find_range(parent);
        int s = std::get<0>(se) + 1;
        int e = std::get<1>(se) + 1;

        qDebug("\tclose range(1-based): [%d, %d]", s+1, e);

        buf_set_lines(s, e, true, {});
        // ref to https://github.com/equalsraf/neovim-qt/issues/596
        api->nvim_win_set_cursor(0, QPoint{0, s});
        erase_entrylist(s, e);

        FileItem &father = *m_fileitem[parent];
        father.opened_tree = false;
        const QString & p = father.fi.absoluteFilePath();
        if (expandStore.contains(p) && expandStore[p]) {
            expandStore[p] = false;
        }
        redraw_line(parent, parent + 1);
    }
    return;
}

void Tree::open(const QList<QVariant> &args)
{
    save_cursor();
    const int l = ctx.cursor - 1;
    const QFileInfo &fi = m_fileitem[l]->fi;
    if (fi.isDir()) {
        changeRoot(fi.absoluteFilePath());
    }
    else if (args.size()>0 && args[0]=="vsplit") {
        QString path = fi.absoluteFilePath();
        qDebug() << "vsplit" << path;
        api->vim_call_function("tree#util#execute_path", {"rightbelow vsplit", path});
    }
    else {
        QString path = fi.absoluteFilePath();
        api->vim_call_function("tree#util#execute_path", {"drop", path});
    }
}


void Tree::open_or_close_tree_recursively(const QList<QVariant>& args)
{
    const int l = ctx.cursor - 1;
    qDebug() << __PRETTY_FUNCTION__;
    assert(0 <= l && l < m_fileitem.size());
    if (l == 0) return;
    QList<QByteArray> ret;
    FileItem &cur = *m_fileitem[l];

    if (cur.fi.isDir() && !cur.opened_tree) {
        cur.opened_tree = true;
        const QString &rootPath = cur.fi.absoluteFilePath();
        expandStore.insert(rootPath, true);
        redraw_line(l, l + 1);
        QList<FileItem*> child_fileitem;
        expandRecursively(cur, child_fileitem);
        int file_count = child_fileitem.size();
        for (int i = 0; i < file_count; ++i)
            m_fileitem.insert(l + 1 + i, child_fileitem[i]);

        if (file_count <= 0) {
            return;
        }

        insert_entrylist(child_fileitem, l + 1, ret);
        buf_set_lines(l+1, l+1, true, ret);
        hline(l + 1, l + 1 + ret.size());
        return;
    }
    else if (cur.opened_tree) {
        const QString &p = cur.fi.absoluteFilePath();
        if (expandStore.contains(p) && expandStore[p]) {
            expandStore[p] = false;
        }
        std::tuple<int, int> se = find_range(l);
        int s = std::get<0>(se) + 1;
        int e = std::get<1>(se) + 1;
        qDebug("\tclose range(1-based): [%d, %d]", s+1, e);
        buf_set_lines(s, e, true, {});
        shrinkRecursively(p);
        erase_entrylist(s, e);
        cur.opened_tree = false;
        redraw_line(l, l + 1);
        return;
    }
    else if (find_parent(l) >= 0) {
        int parent = find_parent(l);
        std::tuple<int, int> se = find_range(parent);
        int s = std::get<0>(se) + 1;
        int e = std::get<1>(se) + 1;

        qDebug("\tclose range(1-based): [%d, %d]", s+1, e);

        buf_set_lines(s, e, true, {});
        // ref to https://github.com/equalsraf/neovim-qt/issues/596
        api->nvim_win_set_cursor(0, QPoint{0, s});

        FileItem &father = *m_fileitem[parent];
        father.opened_tree = false;
        const QString &p = father.fi.absoluteFilePath();
        if (expandStore.contains(p) && expandStore[p]) {
            expandStore[p] = false;
        }
        shrinkRecursively(p);
        erase_entrylist(s, e);
        redraw_line(parent, parent + 1);
        return;
    }

    return;
}

void Tree::goto_(const QList<QVariant> &args)
{
    const int l = ctx.cursor - 1;
    const QFileInfo &fi = m_fileitem[l]->fi;

    if (args.size()>0) {
        QString dest = args.at(0).toString();

        if (dest=="parent") {
            int p = find_parent(l);
            api->nvim_win_set_cursor(0, QPoint{0, p+1});
        }
    }
    else {
    }
}
} // namespace NeovimQt
